package priv.pfz.basedao.info;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import priv.pfz.basedao.annotations.*;
import priv.pfz.basedao.apt.AptUtils;
import priv.pfz.basedao.generator.BytecodeGenerator;
import priv.pfz.basedao.sharding.ShardingStrategy;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.text.MessageFormat;
import java.util.*;
import java.util.function.Function;

/**
 * @author yyjxp
 * 2024/1/31 14:45
 */
public abstract class AbstractDaoInfoParser {

    public DaoInfo parseDaoInfo(Class<?> daoClass) {
        DaoInfo info = new DaoInfo();
        setTableInfo(info, daoClass);
        Class<?> entityClass = getEntityClass(daoClass);
        info.setEntityClass(entityClass);
        Map<String, FieldInfo> fieldInfoMap = getFieldInfoMap(entityClass);
        List<FieldInfo> fieldInfos = Lists.newArrayList(fieldInfoMap.values());
        info.setFieldInfoMap(fieldInfoMap);
        info.setFieldInfos(fieldInfos);
        List<String> selectColumns = Lists.newArrayList();
        for (FieldInfo fieldInfo : fieldInfos) {
            if (!fieldInfo.isNoSelect()) {
                selectColumns.add(getSelectColumn(fieldInfo));
            }
            if (fieldInfo.isUniqueKey()) {
                info.getUniqueKeyFieldInfos().add(fieldInfo);
            }
            if (fieldInfo.getShardingStrategy() != null) {
                info.setShardingKeyFieldInfo(fieldInfo);
            }
        }
        info.setSelectColumns(selectColumns.toArray(new String[0]));
        info.setEntityValueMapGetter(BytecodeGenerator.getUtilMethod(entityClass, fieldInfos));
        return info;
    }

    /**
     * 根据Dao获取表名
     * 1、优先从@Table注解中获取
     * 2、根据Dao类名推断，例如TradeInfoXXX表名为trade_info
     */
    private void setTableInfo(DaoInfo daoInfo, Class<?> daoClass) {
        Table table = daoClass.getAnnotation(Table.class);
        if (table != null) {
            daoInfo.setDbName(table.dbName());
            daoInfo.setTableName(table.tableName());
            if (StringUtils.isBlank(daoInfo.getTableName())) {
                daoInfo.setTableName(getDefaultTableName(daoClass));
            }
            daoInfo.setEnableLogicDelete(table.enableLogicDelete());
            daoInfo.setLogicDeleteColumnName(table.logicDeleteColumnName());
            if (StringUtils.isBlank(daoInfo.getLogicDeleteColumnName())) {
                daoInfo.setLogicDeleteColumnName("delete_flag");
            }
            daoInfo.setShardingMode(table.shardingMode());
        } else {
            daoInfo.setTableName(getDefaultTableName(daoClass));
        }
    }

    protected abstract String getDefaultTableName(Class<?> daoClass);

    private String getSelectColumn(FieldInfo fieldInfo) {
        if (fieldInfo.isAssignedColumnName()) {
            return "`" + fieldInfo.getColumnName() + "` AS " + fieldInfo.getFieldName();
        } else {
            return "`" + fieldInfo.getColumnName() + "`";
        }
    }

    protected abstract Class<?> getEntityClass(Class<?> daoClass);

    /**
     * 解析Entity类的属性
     * 1、serialVersionUID字段不计入
     * 2、static修饰的字段不计入
     * 3、transient修饰的字段不计入
     * 4、@NoColumn标注的字段不计入
     * 5、支持父类中的字段（子类覆盖父类的情况，以子类为准）
     */
    private Map<String, FieldInfo> getFieldInfoMap(Class<?> entityClass) {
        Set<String> handledSet = new HashSet<>();
        Map<String, FieldInfo> fieldInfoMap = new LinkedHashMap<>();
        Set<String> shardingKeys = new HashSet<>();
        Map<String, Method> getterMap = getGetterMap(entityClass);
        while (entityClass != Object.class) {
            for (Field field : entityClass.getDeclaredFields()) {
                String fieldName = field.getName();
                if (!handledSet.add(fieldName)) {
                    continue;
                }
                if (fieldName.equals("serialVersionUID")) {
                    continue;
                }
                if (Modifier.isStatic(field.getModifiers())) {
                    continue;
                }
                if (Modifier.isTransient(field.getModifiers())) {
                    continue;
                }
                if (field.getAnnotation(NoColumn.class) != null) {
                    continue;
                }
                field.setAccessible(true);
                FieldInfo fieldInfo = new FieldInfo();
                fieldInfo.setFieldName(fieldName);
                fieldInfo.setField(field);
                setColumnName(fieldInfo);
                fieldInfo.setGetter(getterMap.get(fieldName));
                fieldInfo.setGetterFunction(getFunction(fieldInfo.getGetter()));
                fieldInfo.setNoSelect(field.getAnnotation(NoSelect.class) != null);
                fieldInfo.setUniqueKey(field.getAnnotation(UniqueKey.class) != null);
                setShardingStrategy(fieldInfo, shardingKeys);
                fieldInfoMap.put(fieldName, fieldInfo);
            }
            entityClass = entityClass.getSuperclass();
        }
        return fieldInfoMap;
    }

    private Map<String, Method> getGetterMap(Class<?> entityClass) {
        try {
            Map<String, Method> getterMap = Maps.newHashMap();
            BeanInfo beanInfo = Introspector.getBeanInfo(entityClass);
            for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
                getterMap.put(pd.getName(), pd.getReadMethod());
            }
            return getterMap;
        } catch (IntrospectionException e) {
            throw new RuntimeException(e);
        }
    }

    private void setColumnName(FieldInfo fieldInfo) {
        Column column = fieldInfo.getField().getAnnotation(Column.class);
        if (column != null && StringUtils.isNotBlank(column.columnName())) {
            fieldInfo.setColumnName(column.columnName());
            fieldInfo.setAssignedColumnName(true);
        } else {
            fieldInfo.setColumnName(AptUtils.fieldToColumn(fieldInfo.getFieldName()));
        }
    }

    private Function getFunction(Method getter) {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            MethodHandle methodHandle = lookup.unreflect(getter);
            return (Function) LambdaMetafactory.metafactory(
                    lookup,
                    "apply",
                    MethodType.methodType(Function.class),
                    MethodType.methodType(Object.class, Object.class),
                    methodHandle,
                    methodHandle.type()).getTarget().invoke();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    private void setShardingStrategy(FieldInfo fieldInfo, Set<String> shardingKeys) {
        ShardingKey shardingKey = fieldInfo.getField().getAnnotation(ShardingKey.class);
        if (shardingKey == null) {
            return;
        }
        if (fieldInfo.getFieldName().equals("id")) {
            throw new RuntimeException("数据库自增id不能作为切分键");
        }
        if (!shardingKeys.isEmpty()) {
            throw new RuntimeException(MessageFormat.format("已存在切分键{0},不支持多切分键", shardingKeys));
        }
        shardingKeys.add(fieldInfo.getFieldName());
        try {
            fieldInfo.setShardingStrategy((ShardingStrategy<Object>) shardingKey.shardingStrategy().newInstance());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
